import 'ast.dart';
import 'yacc_class.dart';

class Transition {
  String name;
  Transition parent;
  bool isToken = false;
  DartCode dartCode;
}

class PhiTransition extends Transition {
  var options = <Transition>[];
}

class StateMachineBuilder {
  final Yacc yacc;
  var _altCache = <Alternative, Transition>{};
  var _ruleCache = <Rule, List<Transition>>{};
  var _refCache = <RuleReference, Transition>{};

  StateMachineBuilder(this.yacc);

  List<Transition> compile(String name, Rule rule) {
    return _ruleCache.putIfAbsent(rule, () {
      var out = <Transition>[];
      for (var alt in rule.alternatives) {
        var compiled = compileAlt(alt);
        out.add(Transition()
          ..name = name
          ..parent = compiled);
      }
      return out;
    });
  }

  Transition compileAlt(Alternative alt) {
    return _altCache.putIfAbsent(alt, () {
      Transition out;
      for (var ref in alt.references) {
        var t = compileRef(ref);
        if (out == null) {
          out = t;
        } else {
          t.parent = out;
          out = t;
        }
      }
      return out;
    });
  }

  Transition compileRef(RuleReference ref) {
    return _refCache.putIfAbsent(ref, () {
      String refName;
      if (ref.string != null) {
        refName = yacc.tokenAliases[ref.string] ??=
            throw "No token has the alias '${ref.string}'.";
      } else {
        refName = ref.reference;
      }

      var isToken = yacc.tokenTypes.containsKey(refName);
      var isRule =
          yacc.grammar.rulesSection.rules.any((r) => r.reference == refName);

      if (isToken) {
        return Transition()
          ..name = refName
          ..isToken = true;
      } else if (isRule) {
        var rule = yacc.grammar.rulesSection.rules
            .firstWhere((r) => r.reference == refName);
        var possible = compile(refName, rule);
        if (possible.length == 1) {
          return possible[0];
        }
        return PhiTransition()..options = possible;
      } else {
        throw 'Unknown token or rule name: "$refName". (Expected ${yacc.grammar.rulesSection.rules.map((r) => r.reference).join(', ')})';
      }
    });
  }
}
